using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;


namespace Agron.Services.Broadcasting
{
    /// <summary>
    /// An implementation of a Windows Hook for Mouse events.
    /// </summary>
    public sealed class MouseHook : IDisposable
    {
        private const int WH_MOUSE_LL = 0xE;

        private SafeMouseHookHandle _hookHandle;
        private LowLevelMouseProc _hookProc;

        /// <summary>
        /// Initializes a new instance of the <see cref="MouseHook"/> class.
        /// </summary>
        /// <remarks>
        /// Locked constructor to force callers to use <see cref="MouseHook.Initialize"/> to initialize the class
        /// since the operation may throw a <see cref="Win32Exception"/> if the hook initialization fails.
        /// </remarks>
        private MouseHook() { }

        /// <summary>
        /// Initializes a new instance of the <see cref="MouseHook"/> class.
        /// </summary>
        /// <returns>Returns a new instance of the <see cref="MouseHook"/> class.</returns>
        /// <exception cref="Win32Exception">If the hook initialization (<c>User32::SetWindowsHookEx</c>) fails.</exception>
        public static MouseHook Initialize()
        {
            MouseHook classInstance = new MouseHook();

            // Create the callback and make sure it doesn't get GC'd,
            // since it'll be called from unmanaged code.
            classInstance._hookProc = new LowLevelMouseProc(classInstance.HookCallback);

            // Set the hook for just the GUI thread
            using (Process currentProcess = Process.GetCurrentProcess())
            using (ProcessModule currentModule = currentProcess.MainModule)
            {
                classInstance._hookHandle = NativeMethods.SetWindowsHookEx(
                    MouseHook.WH_MOUSE_LL, 
                    classInstance._hookProc, 
                    NativeMethods.GetModuleHandle(currentModule.ModuleName), 
                    0);
            }

            if (classInstance._hookHandle.IsInvalid)
            {
                Win32Exception exception = new Win32Exception();
                classInstance.Dispose();
                throw exception;
            }

            return classInstance;
        }

        /// <summary>
        /// Releases unmanaged resources and performs other cleanup operations before the
        /// <see cref="MouseHook"/> is reclaimed by garbage collection.
        /// </summary>
        ~MouseHook()
        {
            this.Dispose();
        }

        /// <summary>
        /// Occurs when a mouse button is pressed.
        /// </summary>
        public event EventHandler<MouseEventArgs> MouseDown;

        /// <summary>
        /// Occurs when the mouse pointer is moved.
        /// </summary>
        public event EventHandler<MouseEventArgs> MouseMove;

        /// <summary>
        /// Occurs when a mouse button is released.
        /// </summary>
        public event EventHandler<MouseEventArgs> MouseUp;

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (null != _hookHandle)
            {
                this._hookHandle.Dispose();
                this._hookHandle = null;
            }

            GC.SuppressFinalize(this);
        }

        private void OnMouseDown(MouseEventArgs e)
        {
            EventHandler<MouseEventArgs> eventHandler = this.MouseDown;

            if (null != eventHandler)
                eventHandler(this, e);
        }

        private void OnMouseMove(MouseEventArgs e)
        {
            EventHandler<MouseEventArgs> eventHandler = this.MouseMove;

            if (null != eventHandler)
                eventHandler(this, e);
        }

        private void OnMouseUp(MouseEventArgs e)
        {
            EventHandler<MouseEventArgs> eventHandler = this.MouseUp;

            if (null != eventHandler)
                eventHandler(this, e);
        }

        private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
        {
            MSLLHOOKSTRUCT hookData;

            if (0 < nCode)
            {
                WindowsMessages message = (WindowsMessages) wParam;
                switch (message)
                {
                    case WindowsMessages.WM_LBUTTONDOWN:
                        hookData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                        this.OnMouseDown(new MouseEventArgs(MouseButtons.Left, 1, hookData.pt.X, hookData.pt.Y, 0));
                        break;

                    case WindowsMessages.WM_RBUTTONDOWN:
                        hookData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                        this.OnMouseDown(new MouseEventArgs(MouseButtons.Right, 1, hookData.pt.X, hookData.pt.Y, 0));
                        break;

                    case WindowsMessages.WM_MOUSEMOVE:
                        hookData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                        this.OnMouseMove(new MouseEventArgs(MouseButtons.None, 0, hookData.pt.X, hookData.pt.Y, 0));
                        break;

                    case WindowsMessages.WM_LBUTTONUP:
                        hookData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                        this.OnMouseUp(new MouseEventArgs(MouseButtons.Left, 1, hookData.pt.X, hookData.pt.Y, 0));
                        break;

                    case WindowsMessages.WM_RBUTTONUP:
                        hookData = (MSLLHOOKSTRUCT) Marshal.PtrToStructure(lParam, typeof(MSLLHOOKSTRUCT));
                        this.OnMouseUp(new MouseEventArgs(MouseButtons.Right, 1, hookData.pt.X, hookData.pt.Y, 0));
                        break;
                }
            }

            return NativeMethods.CallNextHookEx(_hookHandle, nCode, wParam, lParam);
        }

        static class NativeMethods
        {
            /// <summary>
            /// The SetWindowsHookEx function installs an application-defined hook procedure into a hook chain.
            /// </summary>
            /// <param name="idHook">Specifies the type of hook procedure to be installed.</param>
            /// <param name="lpfn">Pointer to the hook procedure.</param>
            /// <param name="hMod">Handle to the DLL containing the hook procedure pointed to by the lpfn parameter.</param>
            /// <param name="dwThreadId">Specifies the identifier of the thread with which the hook procedure is to be associated.</param>
            /// <returns>
            /// If the function succeeds, the return value is the handle to the hook procedure; otherwise, 0.
            /// </returns>
            [DllImport("user32.dll", SetLastError = true)]
            internal static extern SafeMouseHookHandle SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);

            /// <summary>
            /// The CallNextHookEx function passes the hook information to the next hook procedure in the current hook chain.
            /// </summary>
            /// <param name="hhk">Handle to the current hook.</param>
            /// <param name="nCode">Specifies the hook code passed to the current hook procedure.</param>
            /// <param name="wParam">Specifies the wParam value passed to the current hook procedure.</param>
            /// <param name="lParam">Specifies the lParam value passed to the current hook procedure.</param>
            /// <returns>
            /// This value is returned by the next hook procedure in the chain.
            /// </returns>
            [DllImport("user32.dll", SetLastError = true)]
            internal static extern IntPtr CallNextHookEx(SafeMouseHookHandle hhk, int nCode, IntPtr wParam, IntPtr lParam);

            /// <summary>
            /// Retrieves a module handle for the specified module. The module must have been loaded by the calling process
            /// </summary>
            /// <param name="lpModuleName">The name of the loaded module (either a .dll or .exe file).</param>
            /// <returns>
            /// If the function succeeds, the return value is a handle to the specified module.
            /// If the function fails, the return value is NULL.
            /// </returns>
            [DllImport("kernel32.dll", SetLastError = true)]
            internal static extern IntPtr GetModuleHandle(string lpModuleName);
        }
    }
}
